package project3;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;

import project3.Util.CellType;

public class Router {
	public static String routerId = "Tor61Router-";
	public static int routerData = 0x0FAC;
	public static long currentRouterId;
	public static int httpProxyPort;
	
	// Responsible for handling the TCP connections with other routers
	private static Map<String, Socket> tcpConn;
	private static Map<String, DataInputStream> tcpConnIn;
	private static Map<String, DataOutputStream> tcpConnOut;
	private static List<Long> currentCreatingList;
	private static Map<Socket, BlockingQueue<Cell>> inSocketBlockQueue;
	private static Map<Socket, BlockingQueue<Cell>> outSocketBlockingQueue;
	private static Map<Integer, Cell> blockingResponse;
	
	private static SourceRouter srt;
	private static IntermediateRouter irt;
	// Only when this router receive the request to the web server, initialize psw.
	// private static ProxyServerWrapper psw;
	
	private static String REGISTER_SERVER = "cse461.cs.washington.edu";
	private static int REGISTER_PORT = 46101;
	
	public static void main(String[] args) {
		if (args.length != 5) {
			System.out.println("Usage: java Router <reg server host/ip> <reg server port> <group number> <instance number> <HTTP Proxy port>");
			System.exit(-1);
		}
		
		REGISTER_SERVER = args[0];
		REGISTER_PORT = Integer.parseInt(args[1]);
		routerData = Integer.parseInt(args[2]);
		httpProxyPort = Integer.parseInt(args[4]);
		
		tcpConn = new ConcurrentHashMap<String, Socket>();
		tcpConnIn = new ConcurrentHashMap<String, DataInputStream>();
		tcpConnOut = new ConcurrentHashMap<String, DataOutputStream>();
		currentCreatingList = new ArrayList<Long>();
		inSocketBlockQueue = new ConcurrentHashMap<Socket, BlockingQueue<Cell>>();
		outSocketBlockingQueue = new ConcurrentHashMap<Socket, BlockingQueue<Cell>>();
		blockingResponse = new ConcurrentHashMap<Integer, Cell>();
		
		currentRouterId = Long.parseLong(args[3]);
		routerId += routerData + "-" + String.format("%04d", currentRouterId);
		for (int i = Integer.parseInt(args[1]); i < 65536; i++) {
			try {
				// i is the port for the port sending to the registration server
				// i + 1 is the port used for the browser listening.
				irt = new IntermediateRouter(i);
				srt = new SourceRouter(i, httpProxyPort, REGISTER_SERVER, REGISTER_PORT, routerId);
				break;
			} catch (Exception e) {
				// TODO: Handle the error here
				irt.clear();
				e.printStackTrace();
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
			}
		}
		
		// Start to listen to the new request from the browser
		irt.listenRouterRequest();
		srt.listenBrowserRequest();
	}
	
	public static void sendMassageToRouter(String ipPort, Cell message) throws Exception {
		if (!tcpConn.containsKey(ipPort) && !currentCreatingList.contains(SourceRouter.getRouterId(ipPort))) {
			// TODO: Connect to the given ip:port
			// TODO: Add the associate inputStream and the outputStream
			
			synchronized (srt) {
				// Register the creation process.
				currentCreatingList.add(SourceRouter.getRouterId(ipPort));
				
				String[] ipAndPort = ipPort.split(":");
				Socket s = new Socket(ipAndPort[0], Integer.parseInt(ipAndPort[1]));
				
				DataInputStream dis = new DataInputStream(s.getInputStream());
				DataOutputStream dos = new DataOutputStream(s.getOutputStream());
				BlockingQueue<Cell> inCell = new LinkedBlockingQueue<Cell>();
				BlockingQueue<Cell> outCell = new LinkedBlockingQueue<Cell>();
				ReaderThread rth = new ReaderThread(inCell, s);
				WriterThread wth = new WriterThread(outCell, s);
				rth.start();
				wth.start();
				
				tcpConn.put(ipPort, s);
				tcpConnIn.put(ipPort, dis);
				tcpConnOut.put(ipPort, dos);
				inSocketBlockQueue.put(s, inCell);
				outSocketBlockingQueue.put(s, outCell);
				
				OpenCell openCell = new OpenCell(0, CellType.OPEN);
				openCell.setFromAgentId(currentRouterId);
				openCell.setToAgentId(SourceRouter.getRouterId(ipPort));
				
				outCell.add(openCell);
				
				// Get the response back, check response
				if (getWantedCell(0).getCellType() == CellType.CREATE_FAILED) {
					s.close();
					dis.close();
					dos.close();
					rth.terminate();
					wth.terminate();
					currentCreatingList.remove(ipPort);
					sendMassageToRouter(ipPort, message);
					return;
				} else {
					// TODO: Tell user the creation is success
					System.out.println("[Router] Connect to router " + ipPort + " success. ");
				}
				
				currentCreatingList.remove(SourceRouter.getRouterId(ipPort));
			}
			
			// Wait for finishing creating.
			while (currentCreatingList.contains(SourceRouter.getRouterId(ipPort))) {
				// TODO: Find a better way to wait and timeout.
			}
		}
		sendMassageToRouter(tcpConn.get(ipPort), message);
	}
	
	public static void sendMassageToRouter(Socket soc, Cell message) {
		outSocketBlockingQueue.get(soc).add(message);
	}
	
	public static boolean isInCreation(long routerId) {
		if (routerId == currentRouterId) return false;
		return currentCreatingList.contains(routerId);
	}
	
	public static void setResponse(Cell curCell) {
		blockingResponse.put(curCell.getCircId(), curCell);
	}
	
	public static void addNewSocket(Socket soc) {
		String ip = soc.getInetAddress().toString().substring(1);
		int port = ((InetSocketAddress) soc.getRemoteSocketAddress()).getPort();
		String ipPort = ip + ":" + port;
		System.out.println("[IMRoutner] New TCP Connection: " + ipPort);
		tcpConn.put(ipPort, soc);
		try {
			tcpConnIn.put(ipPort, new DataInputStream(soc.getInputStream()));
			tcpConnOut.put(ipPort, new DataOutputStream(soc.getOutputStream()));
			
			BlockingQueue<Cell> inCell = new LinkedBlockingQueue<Cell>();
			BlockingQueue<Cell> outCell = new LinkedBlockingQueue<Cell>();
			ReaderThread rth = new ReaderThread(inCell, soc);
			WriterThread wth = new WriterThread(outCell, soc);
			rth.start();
			wth.start();
			
			inSocketBlockQueue.put(soc, inCell);
			outSocketBlockingQueue.put(soc, outCell);
		} catch (IOException e) {
			// TODO: Add Method to hanlde this faliure case
			System.out.println("[ROUTER] Fail to register the sockect");
		}
		
	}

	public static ProxyServerWrapper associateStreamWithWebserver(RelayCell curRelayCell, Socket soc) {
		ProxyServerWrapper psw = null;
		try {
			if (psw == null) {
				psw = new ProxyServerWrapper(10010);
			}
		} catch (Exception e) {
			sendBeginFail(curRelayCell, soc);
			return psw;
		}
		if (!psw.registerStreamId(curRelayCell)) {
			sendBeginFail(curRelayCell, soc);
			return null;
		} else {
			sendBeginSuccess(curRelayCell, soc);
			return psw;
		}
	}
	
	private static void sendBeginFail(RelayCell curRelayCell, Socket soc) {
		sendMassageToRouter(soc, new RelayCell(curRelayCell.getCircId(), curRelayCell.getStreamId(), 0, (byte) 0x0b, new byte[0]));
	}
	
	private static void sendBeginSuccess(RelayCell curRelayCell, Socket soc) {
		sendMassageToRouter(soc, new RelayCell(curRelayCell.getCircId(), curRelayCell.getStreamId(), 0, (byte) 0x04, new byte[0]));		
	}

	public static void endStreamWithWebserver(RelayCell curRelayCell) {
		ProxyServerWrapper.endStreamWithWebserver(curRelayCell);
	}
	
	public static Cell getWantedCell(int cirid) {
		while (!blockingResponse.containsKey(cirid)) {
			// TODO: Figure out a better way to doing the block.
		}
		return blockingResponse.remove(cirid);
	}
	
	public static BlockingQueue<Cell> getOutBlockingQueue(Socket outSocket) {
		return outSocketBlockingQueue.get(outSocket);
	}

	public static void clean(Socket sock) {
		String cleanIpPort = "";
		for (String ipPort : tcpConn.keySet()) {
			if (tcpConn.get(ipPort).equals(sock)) {
				cleanIpPort = ipPort;
				break;
			}
		}
		tcpConn.remove(cleanIpPort);
		try {

			tcpConnIn.remove(cleanIpPort).close();
			tcpConnOut.remove(cleanIpPort).close();
			sock.close();
		} catch (IOException e) {
			System.out.println("[Router] Fail to clean the bad tcp connection. ");
		}
		inSocketBlockQueue.remove(sock);
		outSocketBlockingQueue.remove(sock);
	}

	public static Socket getTcpConn(String routerIpPort) {
		return tcpConn.get(routerIpPort);
	}
}
